Skip to content

refactor(appkit): own asUser OBO wrapping in Plugin proxy#385

Open
MarioCadenas wants to merge 1 commit into
mainfrom
mario/refactor-asuser-proxy
Open

refactor(appkit): own asUser OBO wrapping in Plugin proxy#385
MarioCadenas wants to merge 1 commit into
mainfrom
mario/refactor-asuser-proxy

Conversation

@MarioCadenas
Copy link
Copy Markdown
Collaborator

Move the OBO-side exports() wrapping out of AppKit#wrapWithAsUser and into Plugin.asUser's proxy. The proxy now intercepts reads of exports and returns the underlying exports() result with every function pre-wrapped in the user-context scope. The cross-file USER_CONTEXT_SYMBOL bridge between plugin.ts and appkit.ts is removed, and wrapExportsInUserContext is deleted from appkit.ts.

The two inline Proxy literals in Plugin.asUser (real OBO and dev-mode fallback) collapse into a single _createAsUserProxy(wrapCall) factory that takes a per-call wrap strategy. The real OBO branch uses runInUserContext(userContext, ...); the dev fallback uses otelContext.with(DEV_OBO_FALLBACK_KEY=true, ...).

Behavior preserving: full appkit test suite passes and typecheck is clean. Adds a focused 26-test file covering proxy mechanics: real OBO method calls, exports() interception (class methods, arrows, nested plain objects, class-instance values not recursed, callable exports, null/undefined), AsyncLocalStorage propagation across Promise.all, concurrent user isolation, error cleanup, dev-fallback exports(), and escape-the-proxy boundaries (returned functions not auto-wrapped, return this returns the unwrapped target).

Also drop the now-unused UserContext type re-export from the internal context barrel (not part of the public package entry).

Move the OBO-side exports() wrapping out of AppKit#wrapWithAsUser
and into Plugin.asUser's proxy. The proxy now intercepts reads of
`exports` and returns the underlying exports() result with every
function pre-wrapped in the user-context scope. The cross-file
USER_CONTEXT_SYMBOL bridge between plugin.ts and appkit.ts is
removed, and wrapExportsInUserContext is deleted from appkit.ts.

The two inline Proxy literals in Plugin.asUser (real OBO and
dev-mode fallback) collapse into a single _createAsUserProxy(wrapCall)
factory that takes a per-call wrap strategy. The real OBO branch
uses runInUserContext(userContext, ...); the dev fallback uses
otelContext.with(DEV_OBO_FALLBACK_KEY=true, ...).

Behavior preserving: full appkit test suite passes and typecheck is
clean. Adds a focused 26-test file covering proxy mechanics: real
OBO method calls, exports() interception (class methods, arrows,
nested plain objects, class-instance values not recursed, callable
exports, null/undefined), AsyncLocalStorage propagation across
Promise.all, concurrent user isolation, error cleanup, dev-fallback
exports(), and escape-the-proxy boundaries (returned functions not
auto-wrapped, `return this` returns the unwrapped target).

Also drop the now-unused UserContext type re-export from the
internal context barrel (not part of the public package entry).

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
@MarioCadenas MarioCadenas requested a review from a team as a code owner May 14, 2026 14:36
@MarioCadenas MarioCadenas requested a review from pkosiec May 14, 2026 14:36
Copy link
Copy Markdown
Member

@pkosiec pkosiec left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice one! Tested Lakebase OBO - still works 👍

Before merge, please take a look at the agentic review summary.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: PR #385 — refactor(appkit): own asUser OBO wrapping in Plugin proxy

Context

PR #385 moves OBO-side exports() wrapping out of AppKit#wrapWithAsUser and into Plugin.asUser's proxy. The cross-file USER_CONTEXT_SYMBOL bridge is removed, and two inline Proxy literals (real OBO + dev fallback) collapse into a single _createAsUserProxy(wrapCall) factory. A 26-test file covering proxy mechanics is added.

Scope: 4 files changed in packages/appkit/ — context/index.ts, core/appkit.ts, plugin/plugin.ts, plugin/tests/asUser-proxy.test.ts (new)
Reviewers dispatched: correctness, testing, maintainability, project-standards, agent-native, learnings-researcher, kieran-typescript, reliability, api-contract


Review Findings

P2 — Moderate

# File Issue Reviewer(s) Confidence Route
1 packages/appkit/src/plugin/plugin.ts:52 Duplicate isPlainObject implementation. Identical logic exists at plugin.ts:52 (module function) and appkit.ts:180 (static method). Two sources of truth for the same check — if logic needs to change, both must be updated independently. maintainability 0.95 manual -> human
2 packages/appkit/src/plugin/plugin.ts:65 wrapExportFunctions mutates input object in-place. Currently safe because exports() returns a fresh object per call, but fragile if a plugin ever caches the result. The docstring says "Mutates and returns" but the wrap naming suggests a non-mutating operation. maintainability 0.80 advisory -> human
3 packages/appkit/src/plugin/plugin.ts:75 No circular reference protection in wrapExportFunctions. Recursive descent into nested plain objects has no cycle detection. Extremely unlikely with real plugin exports (shallow structures, isPlainObject rejects arrays/class instances), but undocumented limitation. testing 0.70 advisory -> human

P3 — Low

# File Issue Reviewer(s) Confidence Route
4 packages/appkit/src/plugin/plugin.ts:70 Redundant Object.hasOwn() check. Object.keys() on line 69 already returns only own enumerable keys, so the hasOwn guard on line 70 can never be true — dead code. testing 0.95 safe_auto -> review-fixer

Pre-existing (not introduced by this PR)

# File Issue Reviewer(s) Confidence
packages/appkit/src/core/appkit.ts:172 (plugin as any).asUser(req).exports() cast loses type safety. Pre-existing pattern — the old code also used (plugin as any) with USER_CONTEXT_SYMBOL. kieran-typescript 0.92

Testing Gaps

  • No test for exports() returning non-plain, non-function values (Map, Set, Array as top-level return) — the fallthrough at plugin.ts:502 is uncovered
  • No test for deeply nested exports (5+ levels) to confirm recursion terminates cleanly
  • No integration test at AppKit layer for when plugin.exports() throws during wrapWithAsUser
  • wrapExportFunctions and isPlainObject have no isolated unit tests (only tested indirectly through proxy integration)

Coverage Notes

  • Suppressed: 0 findings below confidence threshold
  • Learnings: No docs/solutions/ directory exists — no institutional learnings to surface
  • Agent-Native: No gaps — this is an internal refactor with no new user-facing features
  • Schema Drift / Deployment: N/A (no migrations)
  • Failed reviewers: 0 of 9

Verdict: Ready to merge (with optional improvements)

The core refactoring is sound — consolidating two Proxy literals into _createAsUserProxy(wrapCall) is a clear improvement that eliminates the cross-file USER_CONTEXT_SYMBOL bridge. The 26-test file is thorough and well-organized, covering real OBO, dev fallback, exports interception, async propagation, concurrent user isolation, and boundary cases.

No P0 or P1 issues found in the PR's committed changes. The findings below are improvements, not blockers.


Improvement Plan

Fix 1: Remove redundant Object.hasOwn() check (P3)

File: packages/appkit/src/plugin/plugin.ts:70

Remove the dead if (!Object.hasOwn(exports, key)) continue; guard since Object.keys() on line 69 already returns only own enumerable keys.

Optional improvements (not blocking merge)

  1. Deduplicate isPlainObject — Extract to a shared utility (e.g. packages/appkit/src/utils.ts) or have plugin.ts import AppKit.isPlainObject from appkit.ts. Both files (plugin.ts:52 and appkit.ts:180) have identical implementations.

  2. Add test for non-plain-object top-level exports — Add a test case where exports() returns a Map or Array as the top-level value, verifying it's returned as-is without wrapping (the plugin.ts:502 fallthrough path).

  3. Add test for mixed async/sync exports — Verify that an exports object with both async and sync functions preserves their semantics when wrapped.


Verification

After making changes:

pnpm build && pnpm test && pnpm check:fix && pnpm -r typecheck

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants